﻿#region Copyright and License Notice
// Copyright (C)2010-2016 - Rob Levine and other contributors
// http://configgen.codeplex.com
// 
// This file is part of ConfigGen.
// 
// ConfigGen is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// ConfigGen is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License and 
// the GNU Lesser General Public License along with ConfigGen.  
// If not, see <http://www.gnu.org/licenses/>
#endregion

using System;
using System.IO;
using System.Xml;
using ConfigGen.Core.ConfigurationSettings;
using ConfigGen.Core.TemplateLoader;
using ConfigGen.Utilities.Extensions.System;
using ConfigGen.Utilities.Xml;
using log4net;

namespace ConfigGen.Core
{
    /// <summary>
    /// Output writer for writing generated configuraiton files to hard disk (and reading previous version from disk).
    /// </summary>
    public sealed class FileOutputWriter : IOutputWriter<FileOutputWriterResults>
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof (FileOutputWriter));

        private readonly IHashAlgorithmFactory _hashAlgorithmFactory;
        private readonly IXmlStreamFormatter _xmlStreamFormatter;
        private readonly ConfigFileNameGenerator _fileNameGenerator;
        private readonly OutputEncodingFactory _outputEncodingFactory;


        /// <summary>
        /// Initializes a new instance of the <see cref="FileOutputWriter"/> class.
        /// </summary>
        /// <param name="fileNameGenerator">The file name generator.</param>
        /// <param name="hashAlgorithmFactory">The hash algorithm factory.</param>
        /// <param name="xmlStreamFormatter">The XML stream formatter.</param>
        /// <param name="outputEncodingFactory">The output encoding factory.</param>
        public FileOutputWriter(ConfigFileNameGenerator fileNameGenerator, IHashAlgorithmFactory hashAlgorithmFactory, IXmlStreamFormatter xmlStreamFormatter, OutputEncodingFactory outputEncodingFactory) 
        {
            _fileNameGenerator = fileNameGenerator;
            if (hashAlgorithmFactory == null) throw new ArgumentNullException("hashAlgorithmFactory");
            if (xmlStreamFormatter == null) throw new ArgumentNullException("xmlStreamFormatter");
            _hashAlgorithmFactory = hashAlgorithmFactory;
            _xmlStreamFormatter = xmlStreamFormatter;
            _outputEncodingFactory = outputEncodingFactory;
        }

        /// <summary>
        /// Writes the file.
        /// </summary>
        /// <param name="machineConfiguration">The machine configuration.</param>
        /// <param name="generatedConfigStream">The generated config stream.</param>
        /// <param name="settings">The settings.</param>
        /// <param name="templatePreferences">The template preference.</param>
        /// <returns></returns>
        public FileOutputWriterResults WriteFile(MachineConfigurationSettings machineConfiguration, Stream generatedConfigStream, Preferences settings, TemplatePreferences templatePreferences)
        {
            if (settings == null) throw new ArgumentNullException("settings");
            if (templatePreferences == null) throw new ArgumentNullException("templatePreferences");

            MergeTemplatePreferences(templatePreferences, settings);

            var configFilePath = _fileNameGenerator.GenerateFilename(machineConfiguration, settings);

            var results = new FileOutputWriterResults();
            var hashAlgorithm = _hashAlgorithmFactory.GetHashAlgorithm("MD5");

            using (var previousConfigFileVersion = RetrievePreviousConfigFileVersion(configFilePath))
            {
                if (previousConfigFileVersion != null)
                {
                    results.OldMd5 = hashAlgorithm.ComputeHash(previousConfigFileVersion);
                }
            }

            using (Stream tempHoldingStream = new MemoryStream())
            {
                if (templatePreferences.PrettyPrint.Enabled)
                {
                    _xmlStreamFormatter.Initialise(generatedConfigStream, tempHoldingStream);
                    if (templatePreferences.PrettyPrint.Enabled)
                    {
                        _xmlStreamFormatter.FormatterOptions.MaxElementLineLength = templatePreferences.PrettyPrint.LineLength;
                        _xmlStreamFormatter.FormatterOptions.IndentChars = "".PadRight(templatePreferences.PrettyPrint.TabSize, ' ');
                    }
                    _xmlStreamFormatter.Format();
                }
                else if (templatePreferences.OutputProcessing.RemoveRootElement)
                {
                    var writerSettings = new XmlWriterSettings
                                       {
                                           ConformanceLevel = ConformanceLevel.Fragment,
                                           Encoding = _outputEncodingFactory.GetEncoding(settings.OutputEncoding)
                                       };

                    using (var writer = XmlWriter.Create(tempHoldingStream, writerSettings))
                    {
                        // temporary implementation to prove it works!
                        var dom = new XmlDocument();
                        dom.Load(generatedConfigStream);
                        dom.DocumentElement.WriteContentTo(writer);
                    }
                }
                else
                {
                    generatedConfigStream.CopyTo(tempHoldingStream);
                }

                tempHoldingStream.Position = 0;

                results.NewMd5 = hashAlgorithm.ComputeHash(tempHoldingStream);

                if (settings.WriteUnchangedFiles || !results.OldMd5.IsEqualTo(results.NewMd5))
                {
                    tempHoldingStream.Position = 0;
                    WriteNewConfigFileVersion(tempHoldingStream, configFilePath, settings);
                    results.OutputWritten = true;
                    results.GeneratedFile = new FileInfo(configFilePath);
                }
                else
                {
                    results.OutputWritten = false;
                }
            }

            return results;
        }


        /// <summary>
        /// Helper function to merge the settings from the supplied <paramref name="settings"/> parameter, on to the supplied <paramref name="templatePreferences"/> parameter.
        /// </summary>
        /// <param name="templatePreferences">Template preferences instance.</param>
        /// <param name="settings">Settings instance.</param>
        private static void MergeTemplatePreferences(TemplatePreferences templatePreferences, Preferences settings)
        {
            if (templatePreferences == null) throw new ArgumentNullException("templatePreferences");
            if (settings == null) return;

            if (settings.PrettyPrint)
            {
                templatePreferences.PrettyPrint.Enabled = true;
                Log.Debug("User setting overrides template preferences: PrettyPrint: enabled");
            }

            if (settings.PrettyPrintLineLength.HasValue)
            {
                templatePreferences.PrettyPrint.LineLength = settings.PrettyPrintLineLength.Value;
                Log.Debug("User setting overrides template preferences: PrettyPrintLineLength: " + settings.PrettyPrintLineLength.Value);
            }

            if (settings.RemoveRootElement)
            {
                templatePreferences.OutputProcessing.RemoveRootElement = true;
                Log.Debug("User setting overrides template preferences: RemoveRootElement: enabled");
            }

            Log.Debug("OutputWriter template preferences:");
            Log.Debug("PrettyPrint.Enabled: " + templatePreferences.PrettyPrint.Enabled);
            Log.Debug("PrettyPrint.LineLength: " + templatePreferences.PrettyPrint.LineLength);
            Log.Debug("OutputProcessing.RemoveRootElement: " + templatePreferences.OutputProcessing.RemoveRootElement);
        }

        /// <summary>
        /// Returns a stream containing the previous version of a config file, e.g. by reading the existing config file from the disk.
        /// </summary>
        /// <returns>
        /// Previous config file version in a stream, or null if no previous version.
        /// </returns>
        private Stream RetrievePreviousConfigFileVersion(string configFilePath)
        {
            if (File.Exists(configFilePath))
            {
                return new FileStream(configFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            }

            return null;
        }

        private void WriteNewConfigFileVersion(Stream fileContents, string configFilePath, Preferences settings)
        {
            var outputDirectory = new FileInfo(configFilePath).Directory;

            if (settings.InhibitWrite)
            {
                Log.DebugFormat("InhibitWrite is set. Skipping file write for file '{0}'", configFilePath);
                return;
            }

            if (!outputDirectory.Exists)
            {
                Log.DebugFormat("Output directory does not exist. Creating '{0}'", outputDirectory.FullName);
                outputDirectory.Create();
            }

            Log.DebugFormat("Writing file '{0}'", configFilePath);
            using (var outputFileStream = new FileStream(configFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
            {
                fileContents.CopyTo(outputFileStream);
            }
        }
    }
}
